from django.db import models
from django.contrib.auth.models import User
from betterspace.settings import COURSES_APPS, DEFAULT_APPS #gives us the list of installed apps
from apps import TabField, FeatureField

class Role(object):
    """
    Descriptive name of a role a user plays in a course.  Mostly
    the point of this is to be able to easily compare roles.  Use
    the constructor to cast integers and strings to roles.
    
    e.g.
    
    >>> Role('student')
    <Role: "student">
    >>> Role(2)
    <Role: "instructor">
    >>> Role() # or Role(None)
    <Role: "None">
    >>> Role(45)
    <Role: "None">
    >>> Role('student') < 'instructor'
    True
    >>> Role() == 0
    True
    >>> 'admin' < Role('student')
    False
    
    Though max(*roles) and min(*roles) will work, I recommend using
    Role.maxRole and Role.minRole instead, because these are guaranteed
    to return a Role object no matter what.
    
    e.g.
    
    >>> max(Role('instructor'),'student', 0)
    <Role: "instructor">
    >>> min(Role('instructor'),'student', 0)
    0
    >>> Role.minRole(Role('instructor'),'student',0)
    <Role: "None">
    """
    ROLE_CHOICES = [
        None,
        'student',
        'instructor',
        'admin',
    ]
    
    ROLE_CHOICES_ABBR = [
        'N',
        'S',
        'I',
        'A'
    ]
    
    #give django its list of tuples, and exclude the None role.
    MODEL_ROLE_CHOICES = zip(ROLE_CHOICES_ABBR, ROLE_CHOICES)[1:]
    
    def __init__(self, thing=None):
        """
        thing can be an int, a string, or a role.  Always casts to Role.
        If 
        """
        if isinstance(thing, self.__class__):
            self._role = thing._role
        elif thing in range(len(self.ROLE_CHOICES)-1):
            self._role = self.ROLE_CHOICES[thing]
        elif thing in self.ROLE_CHOICES:
            self._role = thing
        else:
            self._role = None
    
    def __int__(self):
        return self.ROLE_CHOICES.index(self._role)
    
    def __bool__(self):
        return not self._role is None
        
    def __cmp__(self, other):
        if isinstance(other, str) or other is None:
            other = Role(other)
        return int(self) - int(other)
    
    def __unicode__(self):
        if self._role is None:
            return 'None'
        return self._role

    def __str__(self):
        return self.__unicode__()    
    
    def __repr__(self):
        return '<Role: "%s">' % self
    
    @classmethod
    def _cast(cls, thing):
        if isinstance(thing, cls):
            return thing
        return cls(thing)
    
    @classmethod
    def _extreme_role(cls, func, *roles):
        #assert isinstance(roles, tuple), 
        roles = tuple([cls(role) for role in roles])
        try:
            return Role(func(roles))
        except TypeError:
            print '\
            _extreme_role was passed a *roles of type %s, and \n\
            here is the variable: %s\
            ' % (type(roles), roles)
            raise
            
    @classmethod
    def minRole(cls, *roles):
        return cls._extreme_role(min, *roles)
    
    @classmethod
    def maxRole(cls, *roles):
        return cls._extreme_role(max, *roles)


class RoleField(models.IntegerField):
    description = "Represents a role in a course."    
    
    __metaclass__ = models.SubfieldBase
    
    def __init__(self, *args, **kwargs):
        kwargs['choices'] = Role.MODEL_ROLE_CHOICES
        super(RoleField, self).__init__(*args, **kwargs)
    
    def to_python(self, value):
        return Role(value)
    
    def get_prep_value(self, value):
        return int(value) #cast the Role to an int.

class Profile(models.Model):
    """
    User's profile.  Acts as an extension of auth.models.User, but with a max_role attribute.
    
    >>> p1 = Profile.create(username='1337H4X0R', 
    ...     email='1337@haxor.com',
    ...     role='student',
    ...     name='Leet Haxor',
    ...     password='fluffybunnies',
    ... )
    >>> p1
    <Profile 1337H4X0R("student")>
    >>> p2 = Profile.create(username='jayferd',
    ...     email='j@yfe.rd',
    ...     role='admin',
    ...     name='John Jay Adkisson',
    ...     password='chunkybacon',
    ... )
    >>> p2.user.first_name
    John Jay
    >>> p2.user.last_name
    Adkisson
    >>> p1.initialed_name
    L. Haxor
    >>> p2.initialed_name
    J. J. Adkisson
    >>> p2.name
    John Jay Adkisson
    >>> p2.url
    /user/jayferd
    >>> p1.role
    <Role: "student">
    >>> p2.role
    <Role: "admin">
    >>> Profile.getByUsername('jayferd')
    <Profile jayferd("admin")>
    """
    max_role = RoleField()
    
    def __unicode__(self):
        return '%s("%s")' % (self.user.username, self.max_role)
    
    @property    
    def name(self,format=0):
        return '%s %s' % (self.user.first_name, self.user.last_name)
    
    @property
    def url(self):
        return '/user/'+self.user.username
    
    @property    
    def initialed_name(self):
            return '. '.join([x[0].upper() for x in s.split(' ')]) + '. ' + self.user.last_name
    
    @property    
    def courses(self):
        mems = Membership.objects.filter(profile=self)
        return [(m.course, m.role) for m in mems]
    
    class UsernameExists(Exception):
        pass
    
    @classmethod
    def create(cls, username, email, password, role='student', 
        name=False, first_name=False, last_name=False
    ):
        """
        Call this guy with Profile.create(username,email,password,role,name)
        Automagically saves everything, so don't use .save() or you will get
        database IntegrityErrors
        """
        #first check that the profile doesn't exist already.
        try:
            p = cls.getByUsername(username)
            #raise cls.UsernameExists
            return p
        except cls.DoesNotExist:
            pass
        
        from django.contrib.auth.models import User
        if name==False:
            if first_name == False and last_name == False:
                first_name=''
                last_name=username
            else:
                first_name = first_name or ''
                last_name = last_name or ''
        else:
            (first_name,last_name) = name.rsplit(' ',1)
        
        u = User.objects.create_user(username,email,password)
        u.first_name,u.last_name = first_name,last_name
        u.save()
        p = cls(user=u,max_role=Role(role))
        p.save()
        return p
        
    @classmethod
    def getByUsername(cls,username):
        """
        Profile.get_by_username('jayferd') will return the profile for 'jayferd'
        """
        return cls.objects.get(user__username=username)
    
    def delete(self, *args, **kwargs):
        """
        Hook to deleting a profile so that the underlying user is deleted too
        """
        self.user.delete()
        return super(Profile, self).delete(*args,**kwargs) #call the real delete() method
    
    def roleByCourse(self,course):
        try:
            m = Membership.objects.get(profile=self,course=course)
            return m.role
        except Membership.DoesNotExist:
            return Role(None)
    
    class PermissionsError(StandardError):
        pass

#################### Course model ######################\

#TODO: create a TabListField and a FeatureListField to use instead of
# SeparatedValuesField.  That'd be a lot cleaner, I think.
class Course(models.Model):
    #from apps import *
    
    slug = models.CharField(max_length=20)
    title = models.CharField(max_length=50)
    instructor = models.ForeignKey(Profile)
    
    
    @property
    def tabs(self):
        return self.tabinstance_set.select_related('tab')
    
    @property
    def features(self):
        return self.featureinstance_set.select_related('feature')
    
    class Meta:
        unique_together = ("instructor", "slug")
        #make sure that the course slug is unique up to the instructor.
    
    def enableApp(self,*apps):
        """
        Use this instead of self.apps.add(app), because I can't get the latter to run
        the hooks.
        """
        for app in apps:
            if isinstance(app, str):
                print 'Grabbing app "%s".' % app
                app = App.objects.get(slug=app.split('.')[-1])
            if app not in self.apps.all():
                print 'Enabling app %s...' % app
                self.apps.append(app)
                if not app.enable_hook is None:
                    print 'Running enable_hook for %s...' % app
                    app.enable_hook(self)        
    
    def disableApp(self,*apps):
        """
        Use this instead of self.apps.remove(app), because I can't get the latter to
        run the hooks
        """
        for app in apps:
            if isinstance(app, str):
                print 'Grabbing app "%s" for disabling.' % app
                app = App.objects.get(slug=app.split('.')[-1])
            if app in self.apps.all():
                if not app.disable_hook is None:
                    app.disable_hook(self)
                self.apps.remove(app)
    
    def save(self, *args, **kwargs):
        App.syncAll()
        
        k = super(Course, self).save(*args,**kwargs)
        
        #sanity check:
        #make sure our instructor actually has permission to be an instructor
        assert self.instructor.max_role >= 'instructor', \
            Profile.PermissionsError('Oops, the profile %s isn\'t allowed to be an instructor.' %
                self.instructor
            )
        
        # now make sure the instructor is a member, at least at level 'instructor'
        # TODO: Do we want to automatically make self.instructor an admin?  Or is 'instructor'
        # okay?
        self.addMember(profile=self.instructor,role='instructor')
        
        # enable all the default apps.  This does nothing
        # if the app is already enabled, so no loss of efficiency.
        for app in DEFAULT_APPS:
            self.enableApp(app)
        
        
        # run the save hooks for the appropriate apps,
        # if they exist
        
        for app in self.apps.all():
            if not app.save_hook is None:
                app.save_hook(self)
        
        return k
        
    def delete(self, *args, **kwargs):
        for a in self.apps.all():
            self.disableApp(a)
        return super(Course,self).delete(*args,**kwargs)
    
    def __unicode__(self):
        return self.title
    
    def get_absolute_url(self):
        return '/%s/%s/' % (self.instructor.user.username, self.slug)
    
    url = property(get_absolute_url)
    
    def addMember(self, profile, role='student'):
        """
        Add a member to the course.  NOTE:  This gets saved to the database
        automatically as a Membership.
        """
        if(profile.user == None):
            profile = profile.get_profile()
        
        role = Role.maxRole('student',Role.minRole(role,profile.max_role))
        #This is makes sure the user's role in the class is not greater than 
        #the max_role determined by their profile.
        
        Membership.addMembership(profile=profile,course=self,role=role)        
    
    #TODO: move this to an app.
    def addNews(self, subject, body, author, app, email=False):
        News(course=self,body=body,subject=subject,author=author, app=app).save()
        if email:
            self._send_email(content=content,subject=subject,author=author,app=app)
    
    def _send_email(self):
        pass
        
    @classmethod    
    def getByNames(cls, instructor,name):
        from django.shortcuts import get_object_or_404
        return get_object_or_404(
            cls,instructor__user__username__iexact=instructor,
            slug__iexact=name
        )
        #__iexact makes it case insensitive
    
    def getMembers(self, level=None):
        return [m.profile for m in Membership.objects.filter(course=self) if m.role >= Role(level)]
    
    members = property(getMembers)

class TabInstance(models.Model):
    tab = TabField()
    course = models.ForeignKey(Course)
    class Meta:
        unique_together = ('tab', 'course')
    #TODO: overload save() and delete() to run the hooks.
    
class FeatureInstance(models.Model):
    feature = FeatureField()
    course = models.ForeignKey(Course)
    class Meta:
        unique_together = ('feature', 'course')    

class Membership(models.Model):
    profile = models.ForeignKey(Profile)
    
    course = models.ForeignKey(Course)
    
    role = RoleField()
    
    def __unicode__(self):
        return 'User %s in %s as %s' % (self.profile.user.username, self.course.title, self.role)
        
    @classmethod
    def addMembership(cls,profile,course,role):
        """
        Used to add profile to course as role.
        """
        #check if we're already a member at some level, and load all those roles
        #into a list
        m = Membership.objects.filter(profile=profile,course=course)
        roles = []
        if m:
            for mem in m:
                roles.append(mem.role)
                mem.delete()
        roles.append(role)
        role = Role.maxRole(*roles)

        #actually add the new membership
        Membership(profile=profile,course=course,role=role).save()


#TODO: move this to an app.
class News(models.Model):
    course = models.ForeignKey(Course)
    #app = models.ForeignKey(App)
    author = models.ForeignKey(Profile)
    
    time = models.DateTimeField(auto_now_add=True) #automatically set to datetime.now() when the News post is created
    
    subject = models.CharField(max_length=60)
    
    body = models.TextField(max_length=1000) #was 1000, changed for mysql 4.1.22

